﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Diagnostics;

using HopfieldNetwork.Core;

namespace HopfieldNetwork
{
    /// <summary>
    /// Red Neuronal de Hopfield
    /// 
    /// Encargada de crear, antenar y operar sobre la red
    /// neuronal
    /// </summary>
    public class HopfieldNeuronalNetwork
    {

        /// <summary>
        /// Firma del Manejador del Evento
        /// 
        /// Asi como este delegado, se debe declarar
        /// el manejador del Evento de cambio de
        /// estado de neurona
        /// </summary>
        /// <param name="sender">Neurona que dispara el evento</param>
        /// <param name="e">Argumentos para el evento</param>
        /// <seealso cref="OnNeuronChangeState"/>
        /// <seealso cref="NeuronChangeStateEventArgs"/>
        public delegate void NeuronChangeState(object sender, NeuronChangeStateEventArgs e);

        /// <summary>
        /// Evento de Cambio de Estado
        /// 
        /// Cuando una Neurona Cambia de Estado, avisa
        /// de ese cambio para hacer cambios pertinentes
        /// </summary>
        /// <seealso cref="NeuronChangeState"/>
        public event NeuronChangeState OnNeuronChangeState;

        /// <summary>
        /// Matriz de Pesos Sinapticos de la
        /// Red Neuronal
        /// </summary>
        SynapticWeightMatrix w;

        /// <summary>
        /// Neuronas dentro dela red
        /// </summary>
        List<Neuron> neurons;

        /// <summary>
        /// Numero de patrondes de entrenamiento
        /// </summary>
        int nPatterns;

        /// <summary>
        /// Numero de neuronas dentro de la red
        /// </summary>
        int nN;

        /// <summary>
        /// indica si el vector de entrada ha sido asignado
        /// para poder comenzar con la operacion de la red
        /// </summary>
        bool isReady;

        /// <summary>
        /// Energia
        /// </summary>
        double energy;

        /// <summary>
        /// Constructor de la red Neurona del Hopfield
        /// 
        /// Construye la red con el numero de neuronas indicadas
        /// </summary>
        /// <param name="nN">Numero de Neuronas que va a tener la Red de 
        /// Hopfield
        /// </param>
        public HopfieldNeuronalNetwork(int nN)
        {
            this.nN = nN;
            neurons = new List<Neuron>(nN);
            for (int i = 0; i < nN; i++)
            {
                Neuron tmpN = new Neuron();
                neurons.Add(tmpN);
            }
            this.w = new SynapticWeightMatrix(this.nN);
            this.nPatterns = 0;
            this.isReady = false;
        }

        /// <summary>
        /// Agregar un nuevo patron de entrenamiento
        /// 
        /// cuando se agrega un nuevo patron de entrenamiento
        /// se actualizan los pesos sinapticos de la matriz
        /// </summary>
        /// <param name="pattern">Vector con el Nuevo Patron de Entrenamiento</param>
        /// <exception cref="InvalidOperationException">Si se agrega un patron de entrenamiento que tenga una
        /// tamano superior o inferior al numero de neuronas</exception>
        public void AddPattern(int[] pattern)
        {
            if (pattern.Length != this.nN)
            {
                throw new InvalidOperationException(string.Format("La red neuronal tiene {0} " +
                                                                  "neuronas, los patrones de entrenamiento " +
                                                                  "deben tener el mismo tamano.", this.nN));
            }
            else
            {
                for (int i = 0; i < this.nN; i++)
                {
                    for (int j = 0; j < this.nN; j++)
                    {
                        W[i, j] += (pattern[i] * pattern[j]);
                    }
                }
                this.nPatterns++;
            }
        }

        /// <summary>
        /// Entradas
        /// 
        /// Valores de entrada a la red neuronal y con los que
        /// se empieza a trabajar
        /// </summary>
        /// <param name="input">vector de entrada</param>
        /// <exception cref="InvalidOperationException">Si se agrega un vector de entrada que tenga un
        /// tamano superior o inferior al numero de neuronas</exception>
        public void Input(int[] input)
        {
            if (input.Length != this.nN)
            {
                throw new InvalidOperationException(string.Format("La red neuronal tiene {0} " +
                                                                  "neuronas, el vector de entrada " +
                                                                  "debe tener el mismo tamano.", this.nN));
            }
            // inicializo las neuronas con el vector
            // de entrada actual
            for (int i = 0; i < this.nN; i++)
            {
                this.neurons[i].State = (input[i] > 0) ? NeuronState.Active : NeuronState.InActive;
            }
            this.isReady = true;
        }

        /// <summary>
        /// Inicia el Funcionamiento de la red
        /// 
        /// realiza el algoritmo de la Red de Hopfield
        /// e itera hasta que converga a algun valor
        /// en este caso, a alguno de los patrones de entrenamiento
        /// </summary>
        /// <exception cref="InvalidOperationException">
        /// Lanza una excepcion si no se ha ingresado el vector
        /// de entrada a la red
        /// </exception>
        /// <seealso cref="HopfieldNetwork.HopfieldNeuronalNetwork.Input"/>
        public void Run()
        {
            //si no se ha ingresado el vector de entrada
            if (!isReady)
            {
                throw new InvalidOperationException("No se han asignado las entradas");
            }
            else
            {
                // numero de cambios en cada iteracion
                int nChange;
                do
                {
                    nChange = 0;
                    for (int i = 0; i < this.nN; i++)
                    {
                        if (this.neurons[i].ChageState(Net(i)))
                        {
                            nChange++;
                            Debug.WriteLine(this.ToString());
                            CalculateEnergy();
                            Debug.WriteLine(this.Energy);
                            OnNeuronChange(new NeuronChangeStateEventArgs(this.Energy));
                        }
                    }
                // si no hay cambios, se termina la busqueda del patron
                // mas parecido al vector de entrada
                } while (nChange>0);
            }
        }

        /// <summary>
        /// Funcion que calcula el estado de toda la red
        /// 
        /// Mira el estado del resto de neuronas interconectadas
        /// a la neurona especificada, para calcular su nuevo estado
        /// </summary>
        /// <param name="idxNeuron">Neurona para la que se calcula</param>
        /// <returns>Valor de la red calculada</returns>
        private int Net(int idxNeuron)
        {
            int net = 0;
            for (int j = 0; j < this.nN; j++)
            {
                net += W[idxNeuron, j] * this.neurons[j].State;
            }
            return net;
        }

        /// <summary>
        /// Calcula la funcion de energia
        /// 
        /// Esta funcion de energia es la propuesta por Lyapunov
        /// y es utilizada para determinar la convergencia de la red
        /// </summary>
        public void CalculateEnergy()
        {
            double tmpE = 0;
            double umbrals = 0;
            for (int i = 0; i < this.nN; i++)
            {
                for (int j = 0; j < this.nN; j++)
                {
                    tmpE += W[i, j] * neurons[i].State * neurons[j].State;
                }
                umbrals += neurons[i].Umbral * neurons[i].State;
            }
            energy = (((-1)* tmpE)/2) + umbrals;
        }

        /// <summary>
        /// Lanza el Evento para avisar el 
        /// cambio de estado de una neurona
        /// </summary>
        /// <param name="args">Argumentos o Parametros del Cambio de Neurona</param>
        /// <seealso cref="OnNeuronChangeState"/>
        /// <seealso cref="NeuronChangeStateEventArgs"/>
        private void OnNeuronChange(NeuronChangeStateEventArgs args)
        {
            if (OnNeuronChangeState != null)
                OnNeuronChangeState.Invoke(null, args);
        }

        /// <summary>
        /// Acceso a la Matris de Pesos Sinapticos
        /// </summary>
        public SynapticWeightMatrix W
        {
            get { return w; }
        }

        /// <summary>
        /// Energia calculada para la red
        /// </summary>
        public double Energy
        {
            get { return energy; }
        }

        /// <summary>
        /// Verifica si la Red esta lista para operar
        /// </summary>
        public bool IsReady
        {
            get { return isReady; }
        }

        /// <summary>
        /// Acceso al Listado de las Neuronas
        /// </summary>
        public List<Neuron> Neurons
        {
            get { return neurons; }
        }

        /// <summary>
        /// Obtiene el numero de neuronas
        /// actuales que tiene la red
        /// </summary>
        public int NumberNeurons
        {
            get { return nN; }
        }

        /// <summary>
        /// Muestra el estado actual de la red de Hopfield
        /// en formato de texto
        /// </summary>
        /// <returns>Estado actual de la red de hopfield</returns>
        public override string ToString()
        {
            StringBuilder strb = new StringBuilder("Estado ACtual = [");
            for (int i = 0; i < this.nN; i++)
            {
                strb.Append(this.neurons[i].State);
                strb.Append(" ");
            }
            strb.Append("]");
            return strb.ToString();
        }
    }
}
